package com.gitee.wsl.flow.redux.dsl

import com.gitee.wsl.flow.redux.reduxStore
import com.gitee.wsl.statemachine.action.Action
import com.gitee.wsl.statemachine.StateMachine
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.receiveAsFlow

@ExperimentalCoroutinesApi
public abstract class FlowReduxStateMachine<S : Any, A : Action>(
    private val initialStateSupplier: () -> S,
) : StateMachine<A, S> {

    public constructor(initialState: S) : this(initialStateSupplier = { initialState })

    private val inputActions = Channel<A>()

    private lateinit var outputState: Flow<S>

    private val activeFlowCounter = atomic(0)

    protected fun spec(specBlock: FlowReduxStoreBuilder<S, A>.() -> Unit) {
        if (::outputState.isInitialized) {
            throw IllegalStateException(
                "State machine spec has already been set. " +
                    "It's only allowed to call spec {...} once.",
            )
        }

        val sideEffectBuilders = FlowReduxStoreBuilder<S, A>().apply(specBlock).sideEffectBuilders

        outputState = inputActions
            .receiveAsFlow()
            .reduxStore(initialStateSupplier, sideEffectBuilders)
            .onStart {
                if (activeFlowCounter.incrementAndGet() > 1) {
                    throw IllegalStateException(
                        "Can not collect state more than once at the same time. Make sure the" +
                            "previous collection is cancelled before starting a new one. " +
                            "Collecting state in parallel would lead to subtle bugs.",
                    )
                }
            }
            .onCompletion {
                activeFlowCounter.decrementAndGet()
            }
    }

     val flowState: Flow<S>
        get() {
            checkSpecBlockSet()
            return outputState
        }

    suspend fun dispatch(action: A) {
        checkSpecBlockSet()
        if (activeFlowCounter.value <= 0) {
            throw IllegalStateException(
                "Cannot dispatch action $action because state Flow of this " +
                    "FlowReduxStateMachine is not collected yet. " +
                    "Start collecting the state Flow before dispatching any action.",
            )
        }
        inputActions.send(action)
    }

    private fun checkSpecBlockSet() {
        if (!::outputState.isInitialized) {
            throw IllegalStateException(
                """
                    No state machine specs are defined. Did you call spec { ... } in init {...}?
                    Example usage:

                    class MyStateMachine : FlowReduxStateMachine<State, Action>(InitialState) {

                        init{
                            spec {
                                inState<FooState> {
                                    on<BarAction> { ... }
                                }
                                ...
                            }
                        }
                    }
                """.trimIndent(),
            )
        }
    }
}
